
--
--  TjQuestLog.lua
--    by Tuhljin
--
--  Library that helps work around the limitations imposed by collapsed headers in the Quest Log (which can prevent
--  your addon from "seeing" all the quests in the log).
--
--  TjQuestLog.SaveHeaderStates( [statesTable] )
--    Remember the current expanded/collapsed state of the quest log's headers. To be used with
--    TjQuestLog.ResetHeaderStates(). Pass a table as the argument to save state data to the table, or omit the
--    argument to use the default table. (The table's other contents will be wiped.)
--
--  TjQuestLog.ResetHeaderStates( [statesTable] )
--    Resets the quest log's headers to the expanded/collapsed state they were in when TjQuestLog.SaveHeaderStates()
--    was called (or when TjQuestLog.GetDataTable(true) was called). Any saved states whose headers are no longer in
--    the log are ignored, as are any headers in the log that weren't there when the states were saved. Pass a table
--    as the argument to load state data from the table, or omit the argument to use the default table. This table
--    would usually be one passed previously to TjQuestLog.SaveHeaderStates(), but can be any table with keys in the
--    format ["Header Name"] = <state>, where <state> evaluates to true if the header was collapsed.
--
--  index, isCollapsed = TjQuestLog.GetHeaderInfo( name )
--    Return information about the header with the given name (string). The first return value is the header's
--    current index; this will become unreliable if the expanded/collasped state of a prior header in the log is
--    later changed. The second return value is a boolean; if it is true, then the header is collapsed.
--
--  LogData = TjQuestLog.GetDataTable( [reliableIndex] )
--    Returns a key-value table containing information about every quest currently in the log. Each key is the Quest
--    ID number of one of the quests (as that used in chat links). In turn, these refer to a table containing the
--    following keys: questTitle, level, questTag, suggestedGroup, isComplete, isDaily, header, index.
--    Their values are as those returned by GetQuestLogTitle() (see http://www.wowwiki.com/API_GetQuestLogTitle)
--    except for header and index; header is a string giving the name of the header it is under (e.g. "Shadowglen"),
--    while index is the quest index number, as that which could be given to GetQuestLogTitle() as an argument when
--    all headers in the log are expanded. (If any are collapsed, the index may be inaccurate.)
--
--    THE TABLE RETURNED HERE IS USED INTERNALLY BY TjQuestLog, SO IT SHOULDN'T BE MANIPULATED. Data is accurate
--    as of the time when GetDataTable() is called, but may become inaccurate over time, becoming accurate again
--    whenever TjQuestLog.GetDataTable() or TjQuestLog(id) is called.
--
--    If reliableIndex evaluates to true, then the quest log's headers will all be expanded after this call, allowing
--    reliable usage of a quest's index. Remember to call TjQuestLog.ResetHeaderStates() after you're done with the
--    index or the player's quest log will not be as he/she left it.
--
--  questTitle, level, questTag, suggestedGroup, isComplete, isDaily, header, index = TjQuestLog(id)
--    Returns data about the quest in the log that has the given quest ID. The return values correspond to the data
--    described under TjQuestLog.GetDataTable(), above. If that quest isn't currently in the log, nothing is
--    returned. (Note that TjQuestLog(id) is actually simply shorthand for TjQuestLog.GetInfo(id).)
--
--  TjQuestLog.Register_OnChange( func[, arg] )
--    Register a function to be called when the player's quest log has changed (and updated data is available). This
--    is useful because the UNIT_QUEST_LOG_CHANGED event is fired before the quest log actually updates, causing the
--    WoW API (and consequently GetDataTable and other TjQuestLog functions) to return old data. The function is
--    passed the given arg as its first (and only) argument. The same function cannot be registered twice but its
--    associated "arg" can be changed by calling Register_OnChange again.
--
--  TjQuestLog.Unregister_OnChange( func )
--    Unregisters a function previously registered by TjQuestLog.Register_OnPlayerQuestLogChange(func). Nothing
--    happens if that function wasn't registered.
--
--  Note: Expanding and collapsing headers, as may occur when any of the above functions except SaveHeaderStates,
--  GetHeaderInfo, or Unregister_OnChange are called, fires QUEST_LOG_UPDATE events. However, TjQuestLog is designed
--  to minimize such manipulation of headers by only doing so when necessary.
--

local THIS_VERSION = 0.4

if (not TjQuestLog or TjQuestLog.Version < THIS_VERSION) then
  TjQuestLog = TjQuestLog or {}
  local TjQuestLog = TjQuestLog
  TjQuestLog.Version = THIS_VERSION

  TjQuestLog.UserHeaderState = TjQuestLog.UserHeaderState or {}
  local UserHeaderState = TjQuestLog.UserHeaderState
  TjQuestLog.HeaderState = TjQuestLog.HeaderState or {}
  local HeaderState = TjQuestLog.HeaderState
  TjQuestLog.QuestLogData = TjQuestLog.QuestLogData or {}
  local QuestLogData = TjQuestLog.QuestLogData
  TjQuestLog.OnChangeFuncs = TjQuestLog.OnChangeFuncs or {}
  local OnChangeFuncs = TjQuestLog.OnChangeFuncs

  local LogIsUpToDate, LOGGED_IN, KeepingTrack

  local function SaveHeaderStates(list)
    wipe(list)
    local numEntries = GetNumQuestLogEntries()
    local name, _, isHeader, isCollapsed
    for i=1,numEntries do
      name, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i)
      if (isHeader) then  list[name] = not not isCollapsed;  end
    end
  end

  local function ResetHeaderStates(list)
    local name, _, isHeader, isCollapsed
    local i = 1
    name, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(1)
    while (name) do  -- Using while loop because the number of entries will change as headers are collapsed/expanded.
      if (isHeader and list[name] ~= nil) then
        if (isCollapsed) then
          if (not list[name]) then  ExpandQuestHeader(i);  end
        elseif (list[name]) then
          CollapseQuestHeader(i);
        end
      end
      i = i + 1
      name, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i)
    end
  end

  local OnChange_Fire, OnEvent
  do
    local numQuests_prev, elapsedTime
    function OnChange_Fire(self, elapsed)
      local numEntries, numQuests = GetNumQuestLogEntries()
      if (numQuests == numQuests_prev) then
      -- Often, just one call to this OnUpdate script is needed but in cases where the quest hasn't been stored
      -- in WoW's cache (questcache.wdb in this case), a new quest's data may still not yet be available through
      -- the WoW API, so we'll check repeatedly for a quest to the log (2 second maximum wait).
        elapsedTime = elapsedTime + elapsed
        if (elapsedTime < 2) then  return;  end
	-- Push ahead if at least 2 seconds have passed even if numQuests hasn't changed.
      end
      self:Hide()
      for func,arg in pairs(OnChangeFuncs) do
        if (arg == OnChangeFuncs) then  -- OnChangeFuncs is the stand-in for nil.
          func()
        else
          func(arg)
        end
      end
    end

    function OnEvent(self, event, arg1, ...)
      if (event == "PLAYER_LOGIN") then
        LOGGED_IN = true
      elseif (arg1 == "player") then
        LogIsUpToDate = nil
        for func in pairs(OnChangeFuncs) do
        -- If there are any registered functions for OnChange:
          local numEntries
          numEntries, numQuests_prev = GetNumQuestLogEntries()
          elapsedTime = 0
          self:Show()
          return;
        end
      end
    end
  end

  if (not TjQuestLog.Frame) then
    local frame = CreateFrame("Frame")
    frame:Hide()
    frame:RegisterEvent("PLAYER_LOGIN")
    frame:SetScript("OnEvent", OnEvent)
    TjQuestLog.Frame = frame
  end

  local idcheck
  local function QuestLogData_Update(resetHeaders)
    if (not KeepingTrack) then
      KeepingTrack = true
      TjQuestLog.Frame:RegisterEvent("UNIT_QUEST_LOG_CHANGED")
      TjQuestLog.Frame:SetScript("OnUpdate", OnChange_Fire)
    end
    if (LogIsUpToDate) then
      return false;  -- No update needed.
    else
      if (resetHeaders) then  SaveHeaderStates(HeaderState);  end
      ExpandQuestHeader(0)  -- Expand all headers
      local numEntries = GetNumQuestLogEntries()
      local questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily
      local header, link, questID, _
      idcheck = idcheck and wipe(idcheck) or {}

      for i=1,numEntries do
        questTitle, level, questTag, suggestedGroup, isHeader, isCollapsed, isComplete, isDaily = GetQuestLogTitle(i)
        if (isHeader) then
          header = questTitle
        else
          link = GetQuestLink(i)
          _, _, questID = strfind(link, "quest:(%d+)")
          questID = tonumber(questID)
          local tab = QuestLogData[questID]
          if (not tab) then  QuestLogData[questID] = {};  tab = QuestLogData[questID];  end
          tab.questTitle = questTitle
          tab.level = level, questTag, suggestedGroup, isComplete, isDaily
          tab.questTag = questTag, suggestedGroup, isComplete, isDaily
          tab.suggestedGroup = suggestedGroup, isComplete, isDaily
          tab.isComplete = isComplete, isDaily
          tab.isDaily = isDaily
          tab.header = header
          tab.index = i
          idcheck[questID] = true
        end
      end
      -- Remove old entries:
      for id,tab in pairs(QuestLogData) do
        if (not idcheck[id]) then  QuestLogData[id] = nil;  end
      end

      if (resetHeaders) then  ResetHeaderStates(HeaderState);  end
      if (LOGGED_IN) then  LogIsUpToDate = true;  end

      return true;  -- Updated.
    end
  end


  function TjQuestLog.SaveHeaderStates(statesTable)
    SaveHeaderStates(statesTable or UserHeaderState)
  end

  function TjQuestLog.ResetHeaderStates(statesTable)
    ResetHeaderStates(statesTable or UserHeaderState)
  end

  function TjQuestLog.GetHeaderInfo(name)
    local numEntries = GetNumQuestLogEntries()
    local n, _, isHeader, isCollapsed
    for i=1,numEntries do
      n, _, _, _, isHeader, isCollapsed = GetQuestLogTitle(i)
      if (isHeader and name == n) then  return i, not not isCollapsed;  end
    end
  end

  function TjQuestLog.GetDataTable(reliableIndex)
    if (reliableIndex) then
      SaveHeaderStates(UserHeaderState)
      if (not QuestLogData_Update()) then
      -- If no update was necessary, expand all headers so reference by index is still reliable:
        ExpandQuestHeader(0)
      end
    else
      QuestLogData_Update(true)
    end
    return QuestLogData
  end

  function TjQuestLog.GetInfo(id, arg2)
    id = type(id) == "number" and id or arg2 or id
    QuestLogData_Update(true)
    local tab = QuestLogData[id]
    if (tab) then
      return tab.questTitle, tab.level, tab.questTag, tab.suggestedGroup, tab.isComplete, tab.isDaily, tab.header, tab.index;
    end
  end
  setmetatable(TjQuestLog, { __call = TjQuestLog.GetInfo })

  function TjQuestLog.Register_OnChange(func, arg)
    assert(type(func) == "function")
    if (not KeepingTrack) then  QuestLogData_Update(true);  end  -- This registers the needed event.
    if (arg == nil) then  arg = OnChangeFuncs;  end  -- OnChangeFuncs is the stand-in for nil.
    OnChangeFuncs[func] = arg
  end

  function TjQuestLog.Unregister_OnChange(func)
    OnChangeFuncs[func] = nil
  end

end
